/*
 * @Author: Liang Huimin
 * @LastEditors: Liang Huimin
 * @email: lhm@cxehr.com
 * @Date: 2024-06-05 16:49:42
 * @LastEditTime: 2024-06-11 13:32:08
 * @motto: Still water run deep
 * @Description: Modify here please
 * @FilePath: \SilkyDeploy\lib\commands\server.js
 */
/*
 * @copyright: Huang Ding
 * @Author: ding-cx
 * @Date: 2020-11-09 09:10:25
 * @LastEditors: Liang Huimin
 * @LastEditTime: 2024-06-06 14:43:27
 * @Description: file content
 */
const { exec } = require('child_process')
const Koa = require('koa')
const inquirer = require('inquirer')
const auth = require('koa-basic-auth')
const koaBody = require('koa-body').default
const archiver = require('archiver')
const compressing = require('compressing')
const dayjs = require('dayjs')
const {
  checkServerConfigExists,
  sleep,
  log,
  succeed,
  error,
  underline
} = require('../utils/index.js')
const do_zip = require('../utils/zip.js')
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const yargs = require('yargs')
const { serverConfigPath, serverConfig } = require('../config')
/**
 * 检查参数是否正确
 * @param {Object} config
 * @returns
 */
const checkConfigCorrect = (config) => {
  let result = true
  const keys = ['name', 'fileName', 'uploadDir', 'deployDir']
  if (config) {
    keys.forEach((key) => {
      if (!config[key] || config[key] === '/') {
        error(`参数错误: ${underline(`${key}属性`)} 不正确`)
        result = false
      }
    })
  } else {
    error('参数错误: 参数不存在')
    result = false
  }
  return result
}
/**
 * 第一步：保存文件
 * @param {*} ctx
 * @param {*} next
 */
const saveFile = async (ctx, next) => {
  log(`(${ctx.url}) 保存文件`)
  const { uploadDir, tempDir } = ctx.app_config
  const zipPath = ctx.request.files.uploadname.filepath
  const upload_path = ctx.app_config.uploadDir + '/' + ctx.app_config.fileName
  if (!(await fs.existsSync(uploadDir))) {
    await fs.mkdirSync(uploadDir, { recursive: true })
  }
  if (await fs.existsSync(upload_path)) {
    await fs.unlinkSync(upload_path)
  }
  await fs.writeFileSync(upload_path, fs.readFileSync(zipPath))
  await next()
}
/**
 * 第二步：执行部署前命令
 * @param {*} ctx
 * @param {*} next
 */
const execBeforeCommand = async (ctx, next) => {
  if (ctx.url === '/deploy') {
    try {
      const { deployBeforeCmd } = ctx.app_config
      if (deployBeforeCmd) {
        log(`(${ctx.url}) 执行部署前命令`)
        if (Array.isArray(deployBeforeCmd) && deployBeforeCmd.length > 0) {
          for (let i = 0; index < deployBeforeCmd.length; i++) {
            await exec(deployBeforeCmd[i])
          }
        } else {
          await exec(deployBeforeCmd)
        }
        succeed('部署前命令执行成功')
      }
      await next()
    } catch (e) {
      error(e)
      ctx.body = e.message
      ctx.status = 500
    }
  } else {
    await next()
  }
}

/**
 * 第三步：备份文件
 * @param {*} ctx
 * @param {*} next
 */
const backupFile = async (ctx, next) => {
  if (ctx.url === '/deploy') {
    try {
      const { deployDir, bakDir, notCover } = ctx.app_config
      const dirName = deployDir.split('/')[deployDir.split('/').length - 1]
      const zipFileName = `${dirName}_bak_${dayjs().format(
        'YYYY-MM-DD_HH:mm:ss'
      )}.zip`
      if (bakDir) {
        log(`(${ctx.url}) 备份文件 ${underline(deployDir)}`)

        await new Promise((resolve, reject) => {
          const archive = archiver('zip', {
            zlib: { level: 9 },
            forceLocalTime: true
          }).on('error', (e) => {
            error('archiver error: ' + e)
            reject(e)
          })

          if (!fs.existsSync(bakDir)) {
            fs.mkdirSync(bakDir)
            log(`(${ctx.url}) 创建临时文件夹成功`)
          }
          let zipFilePath = `${bakDir}/${zipFileName}`
          if (fs.existsSync(zipFilePath)) {
            fs.unlink(zipFilePath, (err) => {
              if (err) {
                error(`(${ctx.url}) 删除临时zip文件出错: ${err}`)
              } else {
                log(`(${ctx.url}) 删除临时zip文件成功`)
              }
            })
          }
          const output = fs.createWriteStream(zipFilePath).on('close', (e) => {
            if (e) {
              error(`(${ctx.url}) 打包zip出错: ${e}`)
              reject(e)
            } else {
              succeed(`(${ctx.url}) ${underline(`${zipFileName}`)} 打包成功`)
              resolve()
            }
          })
          log(`(${ctx.url}) 开始压缩文件`)
          archive.pipe(output)
          archive.directory(deployDir, false)
          archive.finalize()
        })

        succeed(`备份成功 备份至 ${underline(`${bakDir}/${zipFileName}`)}`)
      }
      if (notCover) {
        log(`(${ctx.url}) 备份NotCover文件 ${underline(deployDir)}`)
        const notCoverDir = `${bakDir}/${dirName}_not_cover`
        if (Array.isArray(notCover)) {
          for (let i = 0; i < notCover.length; i++) {
            const fileName = `${deployDir}/${notCover[i]}`
            await fs.copyFileSync(fileName, notCoverDir)
          }
        } else {
          const fileName = `${deployDir}/${notCover}`
          await fs.copyFileSync(fileName, notCoverDir)
        }
        succeed(`备份NotCover成功 备份至 ${underline(notCoverDir)}`)
      }
      await next()
    } catch (e) {
      error(e)
      ctx.body = e.message
      ctx.status = 500
    }
  } else {
    await next()
  }
}

/**
 * 第四步：删除部署文件
 * @param {*} ctx
 * @param {*} next
 */
const removeFile = async (ctx, next) => {
  if (ctx.url === '/deploy') {
    try {
      const { deployDir, isRemoveRemoteFile } = ctx.app_config
      if (isRemoveRemoteFile === 'true') {
        log(`(${ctx.url}) 删除部署文件 ${underline(deployDir)}`)
        if (await fs.existsSync(deployDir)) {
          await fs.rmSync(deployDir, { recursive: true })
          await fs.mkdirSync(deployDir, { recursive: true })
        }
        succeed('删除部署文件成功')
        await next()
      } else {
        await next()
      }
    } catch (e) {
      error(e)
      ctx.body = e.message
      ctx.status = 500
    }
  } else {
    await next()
  }
}

/**
 * 第五步：解压文件
 * @param {*} config
 * @param {*} index
 */
const unzipFile = async (ctx, next) => {
  if (ctx.url === '/deploy') {
    try {
      const { uploadDir, fileName, bakDir, deployDir, notCover } =
        ctx.app_config
      const upload_path = `${uploadDir}/${fileName}`
      const dirName = deployDir.split('/')[deployDir.split('/').length - 1]
      const notCoverDir = `${bakDir}/${dirName}_not_cover`
      await compressing.zip
        .uncompress(upload_path, deployDir)
        .then(() => {
          succeed(`解压文件 ${underline(upload_path)}成功`)
        })
        .catch((err) => {
          error(`解压文件 ${underline(upload_path)}出错: ${err}`)
        })
      if (notCover) {
        log(`(${ctx.url}) 还原NotCover文件 ${underline(deployDir)}`)
        const notCoverDir = `${bakDir}/${dirName}_not_cover`
        if (Array.isArray(notCover)) {
          for (let i = 0; i < notCover.length; i++) {
            const sFileName = `${notCoverDir}/${notCover[i]}`
            const tFileName = `${deployDir}/${notCover[i]}`
            if (fs.existsSync(tFileName)) {
              fs.unlink(tFileName)
            }
            await fs.copyFileSync(sFileName, deployDir)
          }
        } else {
          const sFileName = `${notCoverDir}/${notCover}`
          const tFileName = `${deployDir}/${notCover}`
          if (fs.existsSync(tFileName)) {
            fs.unlink(tFileName)
          }
          await fs.copyFileSync(sFileName, deployDir)
        }
        succeed(`还原NotCover成功 还原至 ${underline(deployDir)}`)
      }
      succeed('部署成功！')
      await next()
    } catch (e) {
      error(e)
      ctx.body = e.message
      ctx.status = 500
    }
  } else {
    await next()
  }
}

/**
 * 第六步：执行部署后命令
 * @param {*} config
 * @param {*} index
 */
const execAfterCommand = async (ctx, next) => {
  if (ctx.url === '/deploy') {
    try {
      const { deployAfterCmd } = ctx.app_config
      if (deployAfterCmd) {
        log(`(${index}) 执行部署前命令`)
        if (Array.isArray(deployAfterCmd) && deployAfterCmd.length > 0) {
          for (let i = 0; index < deployAfterCmd.length; i++) {
            await exec(deployAfterCmd[i])
          }
        } else {
          await exec(deployAfterCmd)
        }
        succeed('部署后命令执行成功')
      } else {
        await next()
      }
    } catch (e) {
      error(e)
      ctx.body = e.message
      ctx.status = 500
    }
  } else {
    await next()
  }
}

const clearTempFile = async (ctx, next) => {
  const { tempDir, bakDir, deployDir } = ctx.app_config
  const zipPath = ctx.request.files.uploadname.filepath
  // 清除临时文件
  if (fs.existsSync(zipPath)) {
    fs.unlinkSync(zipPath)
  }
  const dirName = deployDir.split('/')[deployDir.split('/').length - 1]
  const notCoverDir = `${bakDir}/${dirName}_not_cover`
  if (fs.existsSync(notCoverDir)) {
    fs.unlinkSync(notCoverDir)
  }
  await next()
}
// app runing port
const port = yargs.argv.port || 38907

const app = new Koa()

// 挂载自定义属性在各个中间件均可使用
app.context.app_config = {}

/**
 * 创建服务
 *
 */
const createServer = () => {
  const config = require(serverConfigPath)
  log(`配置文件:${JSON.stringify(config)}`)
  // 内置配置，前端可覆盖deploy_dir和not_cover

  let useConfig = {
    maxFileSize: config.maxFileSize || 8000 * 1024 * 1024, //文件最大大小
    tempDir: config.tempDir, //上传到的文件夹
    historyDir: config.historyDir //旧部署的整体打包备份一下
  }
  if (config.tempDir) {
    if (!fs.existsSync(config.tempDir)) {
      fs.mkdirSync(config.tempDir, { recursive: true })
    }
  }
  if (config.historyDir) {
    if (!fs.existsSync(config.historyDir)) {
      fs.mkdirSync(config.historyDir, { recursive: true })
    }
  }
  app.context.app_config = useConfig
  //计时中间件
  app.use(async (ctx, next) => {
    await next()
    const rt = ctx.response.get('X-Response-Time')
    log(`${ctx.method} ${ctx.url} - ${rt}`)
  })
  app.use(async (ctx, next) => {
    const start = Date.now()
    await next()
    const ms = Date.now() - start
    ctx.set('X-Response-Time', `${ms}ms`)
  })
  app.use(
    auth({ name: config.authName || 'admin', pass: config.authPass || 'admin' })
  )
  /* 使用koabody中间件进行接收文件和临时存储 只有这里处理了，才会有ctx.request.body可访问*/
  app.use(
    koaBody({
      multipart: true,
      formidable: {
        maxFileSize: useConfig.maxFileSize, // 设置上传文件大小最大限制，默认2M
        uploadDir: useConfig.tempDir,
        onError(err) {
          log('koa body中间件接收文件出错', err)
        }
      }
    })
  )
  // 首页
  app.use(async (ctx, next) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
      ctx.body = `${config.serverName} is running`
    } else if (ctx.url === '/favicon.ico') {
      ctx.body = ''
    } else {
      await next()
    }
  })
  /* 配置绑定在ctx上 */
  app.use(async (ctx, next) => {
    let resolveConfig = ctx.request.body
    try {
      resolveConfig = JSON.parse(resolveConfig)
    } catch (error) {}
    // console.log(resolveConfig);
    Object.assign(ctx.app_config, resolveConfig)
    // return;
    // console.log("-----------------------------------------")
    log('部署目录:', ctx.app_config.deploy_dir)
    if (checkConfigCorrect(ctx.app_config)) {
      await next()
    } else {
      ctx.body = '参数错误'
      ctx.status = 500
    }
  })
  //保存文件
  app.use(saveFile)
  // 执行部署前命令
  app.use(execBeforeCommand)
  // 备份文件
  app.use(backupFile)
  //删除旧部署文件
  app.use(removeFile)
  //解压文件
  app.use(unzipFile)
  // 执行部署后命令
  app.use(execAfterCommand)

  app.use(clearTempFile)
  // 最外层处理error
  app.use(async (ctx, next) => {
    try {
      log('请求路径:', ctx.url)
      await next()
      log('处理结束:', ctx.url)
      ctx.body = '部署完成！'
      ctx.status = 200
    } catch (error) {
      ctx.body = error.message
      ctx.status = 500
    }
  })

  app.on('error', (err, ctx) => {
    error(ctx.url + ' server error', err)
  })
  app.listen(config.serverPort)
  succeed(
    `${config.serverName} is running at http://localhost:${config.serverPort}`
  )
}
// 获取用户输入信息
const getUserInputInfo = () => {
  return inquirer.prompt(serverConfig)
}

// 创建JSON对象
const createJsonObj = (userInputInfo) => {
  log(JSON.stringify(userInputInfo))
  const jsonObj = {
    serverName: userInputInfo.serverName,
    serverPort: userInputInfo.serverPort,
    tempDir: userInputInfo.tempDir,
    authName: userInputInfo.authName,
    authPass: userInputInfo.authPass,
    historyDir: userInputInfo.historyDir,
    maxFileSize: 8000 * 1024 * 1024
  }
  return jsonObj
}

// 创建配置文件
const createConfigFile = (jsonObj) => {
  const str = `module.exports = ${JSON.stringify(jsonObj, null, 2)}`
  fs.writeFileSync(serverConfigPath, str)
}

// 格式化配置文件
const formatConfigFile =async () => {
  await childProcess.execSync(`npx prettier --write ${serverConfigPath}`)
}

module.exports = {
  description: '部署服务端',
  apply: () => {
    if (checkServerConfigExists()) {
      createServer()
    } else {
      getUserInputInfo().then(async (userInputInfo) => {
        await createConfigFile(createJsonObj(userInputInfo))
        await formatConfigFile()
        succeed(
          `配置文件生成成功，请查看项目目录下的 ${underline(
            'deploy.server.config.js'
          )} 文件确认配置是否正确`
        )
        createServer();
      })
    }
  }
}
